/*
 * Copyright 2013 Nikolay Todorov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jobinsight.util;
import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;


/**
 * Prints an object to JSON.
 * 
 * @author todorov
 *
 */
public class Printer {
	JsonGenerator g;
	DateFormat  dateFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.MEDIUM,SimpleDateFormat.SHORT);
	Set IGNORE_FIELDS = toSet("serialVersionUID");
	
	/**
	 * Launch the application.
	 */
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					ObjReader reader = new ObjReader();
					Object objToDisplay = reader.getObject();
					File objPath = reader.getFile();
					
					if(objToDisplay!=null)
						start( objPath, objToDisplay );
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	/** 
	 * Deep traverse the objects in non recursive manner to spare stack.
	 * 
	 * @param theObject
	 * @throws Exception
	 */
	public void writeData(Object theObject) throws Exception{
		if(theObject==null)return;
		
		Stack<NodeDescription> stack = new Stack();
		stack.push(new NodeDescription(NodeType.getType(theObject),theObject));
		
		while(!stack.empty()){
			NodeDescription current = stack.pop();
			
			if( needsFinalizer(current) ){ // not a field node, thus finalizer needed
				NodeType ftype = NodeType.getType( current.value, true);
				stack.push(new NodeDescription(ftype, null));
			}
			
			stackChildrenNodes(stack, current);
			
			writeNode(current);
		}
	}
	
	private void stackChildrenNodes(Stack<NodeDescription> stack, NodeDescription current) throws Exception {
		NodeType nodeType = (NodeType) current.type;
		Object value = current.value;
		switch(nodeType){
		
			case OBJ:
				stackObjectFields(stack, value);
				break;
				
			case MAP:
				stackMapElements(stack,(Map)value);
				break;
			
			case ITERABLE:
				stackIterable(stack,(Iterable)value);
				break;
		}
	}

	boolean needsFinalizer(NodeDescription current){
		NodeType nodeType = (NodeType) current.type;
		return nodeType==NodeType.OBJ || nodeType==NodeType.MAP || nodeType==NodeType.ITERABLE;
	}

	private void writeNode(NodeDescription current) throws Exception {
		NodeType nodeType = (NodeType) current.type;
		Object value = current.value;
		
		switch(nodeType){
			case FIELD:
				g.writeFieldName(current.value.toString());
				break;
				
			case OBJ_PRIM:
				writePrimObject(value);
				break;
			case OBJ:
				g.writeStartObject();
				break;
			case FINALIZE_OBJ:
				g.writeEndObject();
				break;
				
			case MAP:
				g.writeStartObject();
				break;
			case FINALIZE_MAP:
				g.writeEndObject();
				break;
			
			case ITERABLE:
				g.writeStartArray();
				break;
			case FINALIZE_ITERABLE:
				g.writeEndArray();
				break;	
		}
	}

	void writePrimObject(Object value) throws Exception {
		if (value == null) {
			g.writeNull();
		} else if (value instanceof Number) {
			if (value instanceof Byte)
				g.writeNumber((Byte) value);
			else if (value instanceof Double)
				g.writeNumber((Double) value);
			else if (value instanceof Integer)
				g.writeNumber((Integer) value);
			else if (value instanceof Float)
				g.writeNumber((Float) value);
			else if (value instanceof Long)
				g.writeNumber((Long) value);
			else if (value instanceof Short)
				g.writeNumber((Short) value);
			else 
				throw new Exception("unknown type:" + value.getClass().getCanonicalName());
		} else if (value instanceof Date) {
			g.writeString(dateFormatter.format((Date) value));
		} else {
			g.writeString(value.toString());
		}
	}
	
	private List<Field> getFields( Class theClass ) {
		List<Field> fieldsList = new ArrayList<Field>();
		if(theClass.getSuperclass()!=null ){
			fieldsList.addAll( getFields( theClass.getSuperclass() ));
		}
		Field[] fields = theClass.getDeclaredFields();
		fieldsList.addAll( Arrays.asList(fields) );
	    return fieldsList;
	}
	
	private void stackObjectFields(Stack<NodeDescription> stack, Object theObject) throws Exception {
		
	    List fieldsList = getFields( theObject.getClass() );

	    Collections.sort(
		    fieldsList,
		    new Comparator() {
		        public int compare(Object o1, Object o2) {
		            return (((Field) o2).getName()).compareTo(((Field) o1).getName());
		        }
		    }
	    );
	    
	    Iterator fieldsIt = fieldsList.iterator();

	    while (fieldsIt.hasNext()) {
	    	Field f = (Field) fieldsIt.next();
		      Class type = f.getType();
		      String fieldName = f.getName();
		      if(IGNORE_FIELDS.contains(fieldName))
		    	  continue;
		      
		      try {
		    	  f.setAccessible(true);
		    	  Object value = f.get(theObject);

		    	  stack.push( new NodeDescription( NodeType.getType(value), value) );
		    	  
		    	  stack.push( new NodeDescription( NodeType.FIELD, fieldName) );
		      } catch (Exception e) {
					e.printStackTrace();
		      }
		}
	}

	public void stackMapElements(Stack<NodeDescription> stack, Map theObject) throws Exception{
		try{
			Map sortedMap = new TreeMap(Collections.reverseOrder());
			sortedMap.putAll(theObject);
			theObject = sortedMap;
		}catch(Throwable e){
			// may be the keys are not Comparable
		}
		
		Iterator keysIt = theObject.keySet().iterator();
		 
		while (keysIt.hasNext()) {
			Object name = (Object) keysIt.next();
			Object value = theObject.get(name);
			stack.push(new NodeDescription(NodeType.getType(value),value));
			stack.push(new NodeDescription(NodeType.FIELD,name));
		}
	}
	
	public void stackIterable(Stack<NodeDescription> stack, Iterable theObject) throws Exception{
		try{
			if(theObject instanceof Set){
				TreeSet sortedset = new TreeSet(Collections.reverseOrder());
				sortedset.addAll((Set)theObject);
				theObject = sortedset; 
			}
		}catch(Throwable e){
			// may be the keys are not Comparable
		}
		Iterator keysIt = theObject.iterator();

		while (keysIt.hasNext()) {
			Object value = (Object) keysIt.next(); 

			stack.push(new NodeDescription( NodeType.getType(value), value));
		}
	}
	
	public static void start(File  objfile, Object objToDisplay){
		try{
			Printer window = new Printer();
			window.initGeneration(objfile);
			window.writeData(objToDisplay);
			window.finalizeGeneration();
		}catch( Exception e ){
			e.printStackTrace();
		}
	}

	void initGeneration(File objfile) throws IOException{
		JsonFactory f = new JsonFactory();
		g = f.createJsonGenerator(new File(objfile+".json"),JsonEncoding.UTF8);
		g.setPrettyPrinter( new DefaultPrettyPrinter() );
	}
	
	void finalizeGeneration() throws IOException{
		g.close();
	}

	public static Set<String> toSet(String... values) {
		return new HashSet<String>(java.util.Arrays.asList(values));
	}

	
	class NodeDescription{
		public NodeType type;
		public Object value;
		
		public NodeDescription( NodeType type, Object value){
			this.type = type;
			this.value = value;
		}

		public String toString(){
			return type.name() + " -> " + value==null?null:value.toString(); 
		}
	}
}